/*
	DeLorme PN-20/40 USB "DeLBin" protocol

    Copyright (C) 2009 Paul Cornett, pc-gpsb at bullseye.com
    Copyright (C) 2005  Robert Lipe, robertlipe@usa.net

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 USA

 */

#include "defs.h"
#include <assert.h>

#define MYNAME "delbin"

/*
Device documentation:
"DeLorme Binary GPS Format", delbin_user_interface_format_176.pdf
obtained here: http://forum.delorme.com/viewtopic.php?t=13846

Notes:
Initial development was done with a PN-40, firmware 2.4.123299. The test
device was upgraded to firmware 2.5.165506 during development.

The "data size" in the message header includes the 4 trailer bytes, so it
is really the size of the whole message minus the header.

Messages do not always start at the beginning of a packet. Every once in a
while, the start of the next message directly follows the end of the previous
one, in the same packet.

The time before an unacknowledged message will be retransmitted by the
device is on the order of 2 to 4 seconds.

Retrieving all tracks at once (using code 0 in message 0xb031) does not
seem to work, it hangs after the first track, maybe waiting for some
undocumented response message.

Character encoding is not documented, appears to be 8859-1.

The undocumented messages 0xaa01, 0xb015, 0xb016 and the use of the
"reserved" byte in message 0xb012 were discovered by examining the data
transferred between the device and DeLorme Topo 8.0. They may have been
added in the PN-40 2.5 firmware.
*/

//-----------------------------------------------------------------------------
// interface to platform-specific device I/O
typedef struct {
	void (*init)(const char* name);
	void (*deinit)(void);
	unsigned (*packet_read)(void*);
	unsigned (*packet_write)(const void*, unsigned);
} delbin_os_ops_t;

// really static, only extern so it can be forward declared
extern delbin_os_ops_t delbin_os_ops;

static unsigned delbin_os_packet_size;
//-----------------------------------------------------------------------------

// number of times to attempt a transfer before giving up
#define ATTEMPT_MAX 2

// debug output: low, medium, high
#define DBGLVL_L 1
#define DBGLVL_M 2
#define DBGLVL_H 3

// Multiple unit support.
#define DELBIN_MAX_UNITS 32
static struct {
	unsigned int unit_number;
	const char *unit_serial_number;
	const char *unit_name;
} delbin_unit_info[DELBIN_MAX_UNITS];
static int n_delbin_units;

#define UNKNOWN_ELEV -2000000

#define sizeofarray(x) (sizeof(x) / sizeof(x[0]))

static char* opt_getposn;
static char* opt_logs;
static char* opt_long_notes;

static arglist_t delbin_args[] = {
	{ "get_posn", &opt_getposn, "Return current position as a waypoint",
		NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
	{ "logs", &opt_logs, "Include groundspeak logs when writing",
		NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
	{ "long_notes", &opt_long_notes, "Use long waypoint notes regardless of PN version",
		NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
	ARG_TERMINATOR
};

// Whether device understands message 0xb016
static int use_extended_notes;

static const char* waypoint_symbol(unsigned index);
static unsigned waypoint_symbol_index(const char* name);
static int track_color(unsigned index);
static unsigned track_color_index(int bgr);

static unsigned waypoint_i;
static unsigned waypoint_n;
static waypoint** wp_array;

//-----------------------------------------------------------------------------
// Message ids and sizes. Only the needed ones are here.
// Note that "in" and "out" ids are named as in the device documentation,
// so "in" means to the device, "out" means from.
#define MSG_ACK 0xaa00
#define MSG_BREAK 0xaa02
#define MSG_BREAK_SIZE 33
#define MSG_NAVIGATION 0xa010
#define MSG_REQUEST_ROUTES 0xb051
#define MSG_REQUEST_ROUTES_SIZE 65
#define MSG_REQUEST_TRACKS 0xb031
#define MSG_REQUEST_TRACKS_SIZE 33
#define MSG_REQUEST_WAYPOINTS 0xb012
#define MSG_REQUEST_WAYPOINTS_SIZE 15
#define MSG_ROUTE_COUNT 0xb050
#define MSG_ROUTE_HEADER_IN 0xb055
#define MSG_ROUTE_HEADER_OUT 0xb052
#define MSG_ROUTE_POINT_IN 0xb056
#define MSG_ROUTE_POINT_OUT 0xb053
#define MSG_ROUTE_SHAPE_IN 0xb057
#define MSG_ROUTE_SHAPE_OUT 0xb054
#define MSG_SATELLITE_INFO 0xa020
#define MSG_TRACK_COUNT 0xb030
#define MSG_TRACK_HEADER_IN 0xb035
#define MSG_TRACK_HEADER_OUT 0xb032
#define MSG_TRACK_POINT_IN 0xb036
#define MSG_TRACK_POINT_OUT 0xb033
#define MSG_TRANSFER_COMPLETE 0xaa04
#define MSG_VERSION 0xa001
#define MSG_WAYPOINT_COUNT 0xb010
#define MSG_WAYPOINT_IN 0xb014
#define MSG_WAYPOINT_OUT 0xb013
// Undocumented:
// This one looks like MSG_ACK, except it also has a string in it that says
// something like "device is busy". The expected MSG_ACK usually immediately
// follows it, so the point of this one is unclear.
#define MSG_NACK 0xaa01
// Long waypoint notes
#define MSG_WAYPOINT_NOTE_IN 0xb016
#define MSG_WAYPOINT_NOTE_OUT 0xb015

//-----------------------------------------------------------------------------
// Message structures

// Output Waypoint Message
// Message ID: 0xB013
// Input Waypoint Message
// Message ID: 0xB014
typedef struct {
	gbuint8 total[4]; // U32
	gbuint8 index[4]; // U32
	gbuint8 year;
	gbuint8 month;
	gbuint8 day;
	gbuint8 hour;
	gbuint8 minute;
	gbuint8 second;
	gbuint8 latitude[4]; // S32 rad * 100000000
	gbuint8 longitude[4]; // S32 rad * 100000000
	gbuint8 elevation[4]; // F32 meters
	gbuint8 color;
	gbuint8 symbol;
	gbuint8 name_size;
	char name[1];
	// note_size[2] U16
	// note[note_size]
} msg_waypoint_t;

// undocumented, seen with PN-40 2.5 firmware
// output waypoint note
// Message ID: 0xB015
// input waypoint note
// Message ID: 0xB016
typedef struct {
	gbuint8 index[2];
	gbuint8 total[2];
	gbuint8 name_size;
	char name[1];
	// note_size[2]
	// note[note_size]
} msg_waypoint_note_t;

// Output Track Point Message
// Message ID: 0xB033
// Input Track Point Message
// Message ID: 0xB036
typedef struct {
	gbuint8 total[4]; // U32
	gbuint8 index[4]; // U32
	gbuint8 number;
	struct {
		gbuint8 year;
		gbuint8 month;
		gbuint8 day;
		gbuint8 hour;
		gbuint8 minute;
		gbuint8 second;
		gbuint8 latitude[4]; // S32 rad * 100000000
		gbuint8 longitude[4]; // S32 rad * 100000000
		gbuint8 elevation[4]; // F32 meters
		gbuint8 speed[2]; // U16 km/h * 10
		gbuint8 heading[2]; // U16 deg * 100
		gbuint8 status;
	} point[1];
} msg_track_point_t;

// Output Track Header (Name) Message
// Message ID: 0xB032
typedef struct {
	gbuint8 total_tracks[2]; // U16
	gbuint8 number[2]; // U16
	char name[32];
	gbuint8 total_points[4]; // U32
	gbuint8 year;
	gbuint8 month;
	gbuint8 day;
	gbuint8 hour;
	gbuint8 minute;
	gbuint8 second;
	gbuint8 color[2]; // U16
	gbuint8 distance[4]; // U32 m
	gbuint8 duration[4]; // U32 sec
	gbuint8 comment_size[2]; // U16
	char comment[1];
} msg_track_header_t;

// Input Upload Track Header Message
// Message ID: 0xB035
typedef struct {
	char name[32];
	gbuint8 total_points[4]; // U32
	gbuint8 year;
	gbuint8 month;
	gbuint8 day;
	gbuint8 hour;
	gbuint8 minute;
	gbuint8 second;
	gbuint8 color[2]; // U16
	gbuint8 comment_size[2]; // U16
	char comment[1];
} msg_track_header_in_t;

// Output Route Shape Message
// Message ID: 0xB054
typedef struct {
	gbuint8 total[4]; // U32
	gbuint8 index[4]; // U32
	gbuint8 number;
	gbuint8 reserved;
	struct {
		gbuint8 latitude[4]; // S32 rad * 100000000
		gbuint8 longitude[4]; // S32 rad * 100000000
	} point[1];
} msg_route_shape_t;

// Output Route Point Message
// Message ID: 0xB053
// Input Route Itin Point Message
// Message ID: 0xB056
typedef struct {
	gbuint8 total[4]; // U32
	gbuint8 index[4]; // U32
	char name[32];
	gbuint8 latitude[4]; // S32 rad * 100000000
	gbuint8 longitude[4]; // S32 rad * 100000000
	gbuint8 time_from_start[4]; // U32 sec
	gbuint8 distance_from_start[4]; // F32 km
	gbuint8 bearing_in[2]; // U16 deg * 100
	gbuint8 bearing_out[2]; // U16 deg * 100
	gbuint8 bearing_next[2]; // U16 deg * 100
	gbuint8 itinerary_type;
	gbuint8 turn_type;
	gbuint8 road_class[2]; // U16
	gbuint8 feature_code[4]; // U32
	gbuint8 exit_label_size;
	char exit_label[1];
	// comment_size U8
	// comment[comment_size]
	// shape_pt_count U32
} msg_route_point_t;

// Output Route Header (Name) Message
// Message ID: 0xB052
typedef struct {
	gbuint8 total[2]; // U16
	gbuint8 index[2]; // U16
	char name[64];
	gbuint8 type;
	gbuint8 total_route_point[4]; // U32
	gbuint8 total_shape_point[4]; // U32
} msg_route_header_t;

// Input Upload Route Header Message
// Message ID: 0xB055
typedef struct {
	char name[64];
	gbuint8 type;
	gbuint8 total_route_point[4]; // U32
	gbuint8 total_shape_point[4]; // U32
} msg_route_header_in_t;

// Output Navigation Message
// Message ID: 0xA010
typedef struct {
	gbuint8 gps_week[2]; // U16
	gbuint8 time_of_week[8]; // D64 sec
	gbuint8 year[2]; // U16
	gbuint8 month;
	gbuint8 day;
	gbuint8 hour;
	gbuint8 minute;
	gbuint8 second;
	gbuint8 satellites;
	gbuint8 latitude[8]; // D64 deg
	gbuint8 longitude[8]; // D64 deg
	gbuint8 elevation[8]; // D64 meters
	gbuint8 geoid_offset[2]; // S16 meters * 10
	gbuint8 speed[4]; // F32 km/h
	gbuint8 heading[2]; // U16 deg * 100
	gbuint8 magnetic_variation[2]; // S16 deg * 100
	gbuint8 fix_status;
} msg_navigation_t;

// Output Satellite Info Message
// Message ID: 0xA020
typedef struct {
	gbuint8 gps_week[2]; // U16
	gbuint8 time_of_week[8]; // D64 sec
	gbuint8 hdop[2]; // U16
	gbuint8 vdop[2]; // U16
	gbuint8 pdop[2]; // U16
	gbuint8 number;
	struct {
		gbuint8 prn;
		gbuint8 azimuth[2]; // S16 deg? * 100
		gbuint8 elevation[2]; // S16 deg? * 100
		gbuint8 Cn0[2]; // U16 snr * 100
		gbuint8 status;
	} sat[1];
} msg_satellite_t;

// Output Version Message
// Message ID: 0xA001
typedef struct {
	gbuint8 firmware_version[4];
	char company[32];
	char product[32];
	char firmware[32];
	char gps_firmware[48];
	char serial[16];
	char extra[16];
} msg_version_t;

//-----------------------------------------------------------------------------

static gbuint16
checksum(const gbuint8* p, unsigned n)
{
	int x = 0;
	unsigned i;
	for (i = n / 2; i > 0; i--) {
		x += *p++;
		x += *p++ << 8;
	}
	if (n & 1) {
		x += *p;
	}
	return (gbuint16)-x;
}

//-----------------------------------------------------------------------------
// OS packet read/write wrappers

static unsigned
packet_read(void* buf)
{
	unsigned n = delbin_os_ops.packet_read(buf);
	if (n == 0) {
		fatal(MYNAME ": read 0\n");
	}
	if (global_opts.debug_level >= DBGLVL_H) {
		unsigned j;
		warning(MYNAME ": pcktrd ");
		for (j = 0; j < n; j++) {
			warning("%02x ", ((gbuint8*)buf)[j]);
		}
		warning("  ");
		for (j = 0; j < n; j++) {
			gbuint8 c = ((gbuint8*)buf)[j];
			warning("%c", isprint(c) ? c : '.');
		}
		warning("\n");
	}
	return n;
}

static void
packet_write(const void* p, unsigned size)
{
	unsigned n;
	if (global_opts.debug_level >= DBGLVL_H) {
		unsigned j;
		warning(MYNAME ": pcktwr ");
		for (j = 0; j < size; j++) {
			warning("%02x ", ((gbuint8*)p)[j]);
		}
		warning("  ");
		for (j = 0; j < size; j++) {
			gbuint8 c = ((gbuint8*)p)[j];
			warning("%c", isprint(c) ? c : '.');
		}
		warning("\n");
	}
	n = delbin_os_ops.packet_write(p, size);
	if (n != size) {
		fatal(MYNAME ": short write %u %u\n", size, n);
	}
}

//-----------------------------------------------------------------------------

// dynamically sized buffer with space reserved for message header and trailer
typedef struct {
	// message data size
	unsigned size;
	// buffer size
	unsigned capacity;
	gbuint8* buf;
	// convenience pointer to message data area
	void* data;
} message_t;

static void
message_init(message_t* m)
{
	m->capacity = 100;
	m->buf = xmalloc(m->capacity);
	m->data = m->buf + 2 + 8;
}

static void
message_init_size(message_t* m, unsigned size)
{
	m->size = size;
	m->capacity = 2 + 8 + size + 4;
	m->buf = xmalloc(m->capacity);
	m->data = m->buf + 2 + 8;
}

static void
message_free(message_t* m)
{
	xfree(m->buf);
	m->buf = NULL;
	m->data = NULL;
}

static void
message_ensure_size(message_t* m, unsigned size)
{
	m->size = size;
	if (m->capacity < 2 + 8 + size + 4) {
		m->capacity = 2 + 8 + size + 4;
		xfree(m->buf);
		m->buf = xmalloc(m->capacity);
		m->data = m->buf + 2 + 8;
	}
}

static unsigned
message_get_id(const message_t* m)
{
	return le_readu16(m->buf + 4);
}

//-----------------------------------------------------------------------------

static void
message_write(unsigned msg_id, message_t* m)
{
	unsigned chksum;
	unsigned count;
	unsigned n;
	gbuint8* p = m->buf;

	// header (2 start bytes filled in later)
	p[2] = 0xdb;
	p[3] = 0xfe;
	le_write16(p + 4, msg_id);
	// "data size" includes 4 trailer bytes
	le_write16(p + 6, m->size + 4);
	chksum = checksum(p + 2, 6);
	le_write16(p + 8, chksum);
	// message data (filled in by caller)
	chksum = checksum(m->data, m->size);
	n = 2 + 8 + m->size;
	// trailer (checksum and marker bytes)
	le_write16(p + n, chksum);
	p[n + 2] = 0xad;
	p[n + 3] = 0xbc;
	// size of message not counting packet start bytes
	count = 8 + m->size + 4;
	do {
		const gbuint8 save0 = p[0];
		const gbuint8 save1 = p[1];
		n = delbin_os_packet_size - 2;
		if (n > count) {
			n = count;
		}
		// doc. says 0x20, device sends 0, probably ignored
		p[0] = 0x20;
		// valid bytes in packet after first 2
		p[1] = n;
		packet_write(p, 2 + n);
		p[0] = save0;
		p[1] = save1;
		p += n;
		count -= n;
	} while (count != 0);
	if (global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": sent %x\n", msg_id);
}

// read from the payload of a single packet
static unsigned
read_depacketize_1(gbuint8** p, unsigned n, int new_packet)
{
	static gbuint8 buf[256];
	static unsigned buf_i, buf_n;
	while (buf_n == 0 || new_packet) {
		packet_read(buf);
		new_packet = FALSE;
		if (buf[1] <= delbin_os_packet_size - 2) {
			buf_n = buf[1];
			buf_i = 2;
		}
	}
	*p = buf + buf_i;
	if (n > buf_n) {
		n = buf_n;
	}
	buf_n -= n;
	buf_i += n;
	return n;
}

// read from packet payloads until request is fulfilled
static void
read_depacketize(gbuint8* buf, unsigned n)
{
	while (n) {
		gbuint8* p;
		unsigned nn = read_depacketize_1(&p, n, FALSE);
		memcpy(buf, p, nn);
		n -= nn;
		buf += nn;
	}
}

// Get one valid message.
// If a corrupted message with the right id is seen, return failure (0).
static unsigned
message_read_1(unsigned msg_id, message_t* m)
{
	unsigned id;
	for (;;) {
		unsigned total;
		gbuint8 buf[8];
		gbuint8* p;

		read_depacketize(buf, 8);
		while (buf[0] != 0xdb || buf[1] != 0xfe || checksum(buf, 6) != le_readu16(buf + 6)) {
			gbuint8* pp;
			// try for a message start at the beginning of next packet
			read_depacketize_1(&pp, 8, TRUE);
			memcpy(buf, pp, 8);
		}
		id = le_readu16(buf + 2);
		total = le_readu16(buf + 4);
		message_ensure_size(m, total - 4);
		// copy in message head, really only need id field, do the rest for debugging
		m->buf[0] = m->buf[1] = 0;
		memcpy(m->buf + 2, buf, 8);
		// read message body and trailer
		read_depacketize(m->data, total);
		p = (gbuint8*)m->data + m->size;
		if (checksum(m->data, m->size) == le_readu16(p) &&
			p[2] == 0xad && p[3] == 0xbc)
		{
			if (global_opts.debug_level >= DBGLVL_M)
				warning(MYNAME ": received %x\n", id);
			break;
		}
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": corrupted message %x\n", id);
		if (id == msg_id) {
			id = 0;
			break;
		}
	}
	return id;
}

// Send MSG_ACK for given message
static void
message_ack(unsigned id, const message_t* m)
{
	message_t ack;
	char* p1;
	const char* p2 = m->data;
	switch (id) {
	case MSG_ACK:
	case MSG_NACK:
	case MSG_NAVIGATION:
	case MSG_SATELLITE_INFO:
		// don't ack these
		return;
	}
	message_init_size(&ack, 4);
	p1 = ack.data;
	// ack payload is id and body checksum of acked message
	le_write16(p1, id);
	p1[2] = p2[m->size];
	p1[3] = p2[m->size + 1];
	message_write(MSG_ACK, &ack);
	message_free(&ack);
}

// Get specific message, ignoring others. Sends ACK for non-interval messages.
// Gives up if 6 navigation messages are received, which means we waited at least
// 5 seconds.
static int
message_read(unsigned msg_id, message_t* m)
{
	unsigned id;
	int interval_message_count = 0;

	if (global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": looking for %x\n", msg_id);
	for (;;) {
		id = message_read_1(msg_id, m);
		if (id == 0) {
			break;
		}
		message_ack(id, m);
		if (id == msg_id) {
			break;
		}
		if (id == MSG_NAVIGATION) {
			interval_message_count++;
			if (interval_message_count == 6) {
				break;
			}
		}
	}
	return id == msg_id;
}

// Read a sequence of messages, up to a MSG_TRANSFER_COMPLETE
static int
get_batch(message_t** array, unsigned* n)
{
	int success = 1;
	unsigned array_max = 100;
	message_t* a = xmalloc(array_max * sizeof(message_t));
	unsigned i = 0;
	unsigned id;
	if (global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": begin get_batch\n");
	do {
		unsigned timeout_count = 0;
		if (i == array_max) {
			message_t* old_a = a;
			array_max += array_max;
			a = xmalloc(array_max * sizeof(message_t));
			memcpy(a, old_a, i * sizeof(message_t));
			xfree(old_a);
		}
		message_init(&a[i]);
		for (;;) {
			id = message_read_1(0, &a[i]);
			switch (id) {
			case MSG_NAVIGATION:
				timeout_count++;
				if (timeout_count == 6) {
					success = 0;
					break;
				}
				// fall through
			case 0:
			case MSG_ACK:
			case MSG_NACK:
			case MSG_SATELLITE_INFO:
				continue;
			}
			break;
		}
		message_ack(id, &a[i]);
		i++;
	} while (success && id != MSG_TRANSFER_COMPLETE);
	if (success) {
		*array = a;
		*n = i - 1;
		message_free(&a[*n]);
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": end get_batch, %u messages\n", *n);
	} else {
		while (i--) {
			message_free(&a[i]);
		}
		xfree(a);
		*array = NULL;
		*n = 0;
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": end get_batch, failed\n");
	}
	return success;
}

static struct {
	unsigned msg_id;
	message_t msg;
} *batch_array;
static unsigned batch_array_max;
static unsigned batch_array_i;

// add a message to sequence that will later be sent all at once
static void
add_to_batch(unsigned id, message_t* m)
{
	if (batch_array_i == batch_array_max) {
		void* old = batch_array;
		if (batch_array_max == 0) {
			batch_array_max = 50;
		}
		batch_array_max += batch_array_max;
		batch_array = xmalloc(batch_array_max * sizeof(*batch_array));
		if (batch_array_i) {
			memcpy(batch_array, old, batch_array_i * sizeof(*batch_array));
			xfree(old);
		}
	}
	batch_array[batch_array_i].msg_id = id;
	batch_array[batch_array_i].msg = *m;
	batch_array_i++;
	memset(m, 0, sizeof(*m));
}

// send an accumulated sequence of messages
static void
send_batch(int expect_transfer_complete)
{
	message_t m;
	const unsigned n = batch_array_i;
	unsigned i;

	message_init(&m);
	if (global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": begin send_batch, %u messages\n", n);
	for (i = 0; i < n; i++) {
		unsigned timeout_count = 0;
		message_write(batch_array[i].msg_id, &batch_array[i].msg);
		for (;;) {
			unsigned id = message_read_1(0, &m);
			switch (id) {
			case MSG_ACK:
				break;
			case MSG_NAVIGATION:
				timeout_count++;
				if (timeout_count > 2) {
					fatal(MYNAME ": send_batch timed out\n");
				}
				if (timeout_count == 2) {
					if (global_opts.debug_level >= DBGLVL_M)
						warning(MYNAME ": re-sending %x\n", batch_array[i].msg_id);
					message_write(batch_array[i].msg_id, &batch_array[i].msg);
				}
				// fall through
			case 0:
			case MSG_NACK:
			case MSG_SATELLITE_INFO:
				continue;
			default:
				warning(MYNAME ": unexpected response message %x during send_batch\n", id);
				continue;
			}
			break;
		}
	}
	if (expect_transfer_complete) {
		message_read(MSG_TRANSFER_COMPLETE, &m);
	}
	if (global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": end send_batch\n");
	for (i = n; i--;) {
		message_free(&batch_array[i].msg);
	}
	xfree(batch_array);
	message_free(&m);
	batch_array_i = batch_array_max = 0;
}

//-----------------------------------------------------------------------------
// Coordinate conversion

static double
delbin_rad2deg(gbint32 x)
{
	return x * ((180 / M_PI) / 100000000);
}

static gbint32
delbin_deg2rad(double x)
{
	return (gbint32)(x * ((M_PI / 180) * 100000000));
}

//-----------------------------------------------------------------------------
// Waypoint reading

static time_t
decode_time(const gbuint8* p)
{
	struct tm t;
	t.tm_year = p[0];
	t.tm_mon  = p[1] - 1;
	t.tm_mday = p[2];
	t.tm_hour = p[3];
	t.tm_min  = p[4];
	t.tm_sec  = p[5];
	return mkgmtime(&t);
}

static waypoint*
decode_waypoint(const void* data)
{
	waypoint* wp = waypt_new();
	const msg_waypoint_t* p = data;
	const char* s;
	float f;

	wp->creation_time = decode_time(&p->year);
	wp->latitude = delbin_rad2deg(le_read32(p->latitude));
	wp->longitude = delbin_rad2deg(le_read32(p->longitude));
	f = le_read_float(p->elevation);
	if (f > UNKNOWN_ELEV) {
		wp->altitude = f;
	}
	wp->icon_descr = waypoint_symbol(p->symbol);
	if (wp->icon_descr) {
		wp->icon_descr = xstrdup(wp->icon_descr);
	}
	if (p->name_size && p->name[0]) {
		wp->description = xstrdup(p->name);
	}
	s = p->name + p->name_size;
	if (le_readu16(s) &&  s[2]) {
		wp->notes = xstrdup(s + 2);
	}
	return wp;
}

static void
read_waypoints(void)
{
	message_t m;
	message_t* msg_array;
	unsigned msg_array_n;
	waypoint* wp = NULL;
	unsigned n_point;
	unsigned notes_i = 0;
	unsigned notes_max = 0;
	unsigned i;
	int attempt = ATTEMPT_MAX;

	message_init(&m);
	// get number of waypoints
	for (;;) {
		m.size = 0;
		message_write(MSG_WAYPOINT_COUNT, &m);
		if (message_read(MSG_WAYPOINT_COUNT, &m))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading waypoint count failed\n");
	}
	n_point = le_readu32(m.data);
	if (global_opts.debug_level >= DBGLVL_L)
		warning(MYNAME ": %u waypoints\n", n_point);
	if (n_point == 0) {
		message_free(&m);
		return;
	}
	// get waypoint messages
	attempt = ATTEMPT_MAX;
	for (;;) {
		m.size = MSG_REQUEST_WAYPOINTS_SIZE;
		memset(m.data, 0, m.size);
		// This byte is documented as reserved. Setting it to 3 is required to get
		// extended notes (message 0xb015) with PN-40 firmware 2.5.
		// Whether it has any effect with earlier firmware or the PN-20 is unknown.
		((char*)m.data)[1] = 3;
		message_write(MSG_REQUEST_WAYPOINTS, &m);
		if (get_batch(&msg_array, &msg_array_n))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading waypoints failed\n");
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": timed out reading waypoints, retrying\n");
		m.size = MSG_BREAK_SIZE;
		memset(m.data, 0, m.size);
		message_write(MSG_BREAK, &m);
	}
	message_free(&m);
	// process waypoint messages
	for (i = 0; i < msg_array_n; i++) {
		unsigned id = message_get_id(&msg_array[i]);
		if (id == MSG_WAYPOINT_OUT) {
			wp = decode_waypoint(msg_array[i].data);
			waypt_add(wp);
			notes_i = 0;
			notes_max = 0;
			if (global_opts.debug_level >= DBGLVL_L)
				warning(MYNAME ": read waypoint '%s'\n", wp->description);
		} else if (wp && id == MSG_WAYPOINT_NOTE_OUT) {
			const msg_waypoint_note_t* p = msg_array[i].data;
			const char* s = p->name + p->name_size;
			unsigned nn = le_readu16(s);
			if (notes_max < notes_i + nn) {
				char* old = wp->notes;
				if (notes_max == 0) {
					notes_max = nn;
				}
				do {
					notes_max += notes_max;
				} while (notes_max < notes_i + nn);
				wp->notes = xmalloc(notes_max);
				if (old) {
					memcpy(wp->notes, old, notes_i);
					xfree(old);
				}
			}
			if (nn) {
				memcpy(wp->notes + notes_i, s + 2, nn);
				notes_i += nn;
				if (wp->notes[notes_i - 1] == 0) {
					notes_i--;
				}
			}
		} else {
			fatal(MYNAME ": unexpected message %x while reading waypoints\n", id);
		}
		message_free(&msg_array[i]);
	}
	xfree(msg_array);
}

//-----------------------------------------------------------------------------
// Waypoint writing

static void
encode_time(time_t time_, gbuint8* p)
{
	const struct tm* t = gmtime(&time_);
	p[0] = t->tm_year;
	p[1] = t->tm_mon + 1;
	p[2] = t->tm_mday;
	p[3] = t->tm_hour;
	p[4] = t->tm_min;
	p[5] = t->tm_sec;
}

static void
get_gc_notes(const waypoint* wp, int* symbol, char** notes, unsigned* notes_size)
{
	fs_xml* fs_gpx;
	xml_tag* root = NULL;
	gbfile* fd = gbfopen(NULL, "w", MYNAME);
	const char* size = NULL;
	int gc_sym = 0;

	switch (wp->gc_data->type) {
	case gt_traditional: gc_sym = 160; break;
	case gt_multi: gc_sym = 161; break;
	case gt_virtual: gc_sym = 169; break;
	case gt_letterbox: gc_sym = 163; break;
	case gt_event: gc_sym = 165; break;
	case gt_suprise: gc_sym = 162; break;
	case gt_webcam: gc_sym = 170; break;
	case gt_earth: gc_sym = 168; break;
	case gt_benchmark: gc_sym = 172; break;
	case gt_cito: gc_sym = 167; break;
	case gt_mega: gc_sym = 166; break;
	case gt_unknown:
	case gt_locationless:
	case gt_ape:
		break;
	}
	if (wp->description) {
		gbfputs(wp->description, fd);
		if (wp->gc_data->placer) {
			gbfprintf(fd, " by %s", wp->gc_data->placer);
		}
		gbfputc('\n', fd);
	}
	gbfprintf(fd, "Cache ID: %s\n", wp->shortname);
	if (gc_sym) {
		gbfprintf(fd, "%s\n", waypoint_symbol(gc_sym));
		*symbol = gc_sym;
	} else if (wp->icon_descr) {
		gbfprintf(fd, "%s\n", wp->icon_descr);
	}
	switch (wp->gc_data->container) {
	case gc_micro: size = "Micro"; break;
	case gc_small: size = "Small"; break;
	case gc_regular: size = "Regular"; break;
	case gc_large: size = "Large"; break;
	case gc_unknown:
	case gc_other:
	case gc_virtual:
		break;
	}
	if (size) {
		gbfprintf(fd, "SIZE: %s\n", size);
	}
	if (wp->gc_data->diff % 10) {
		gbfprintf(fd, "D%.1f", wp->gc_data->diff / 10.0);
	} else {
		gbfprintf(fd, "D%u", wp->gc_data->diff / 10);
	}
	if (wp->gc_data->terr % 10) {
		gbfprintf(fd, "/T%.1f\n", wp->gc_data->terr / 10.0);
	} else {
		gbfprintf(fd, "/T%u\n", wp->gc_data->terr / 10);
	}
	if (wp->gc_data->hint) {
		gbfprintf(fd, "HINT: %s\n", wp->gc_data->hint);
	}
	if (wp->gc_data->desc_short.utfstring || wp->gc_data->desc_long.utfstring) {
		gbfputs("DESC: ", fd);
		if (wp->gc_data->desc_short.utfstring) {
			char* s1 = strip_html(&wp->gc_data->desc_short);
			char* s2 = cet_str_utf8_to_any(s1, global_opts.charset);
			gbfprintf(fd, "%s\n", s2);
			xfree(s2);
			xfree(s1);
		}
		if (wp->gc_data->desc_long.utfstring) {
			char* s1 = strip_html(&wp->gc_data->desc_long);
			char* s2 = cet_str_utf8_to_any(s1, global_opts.charset);
			gbfputs(s2, fd);
			xfree(s2);
			xfree(s1);
		}
	}
	fs_gpx = (fs_xml*)fs_chain_find(wp->fs, FS_GPX);
	if (opt_logs && fs_gpx && fs_gpx->tag) {
		root = xml_findfirst(fs_gpx->tag, "groundspeak:logs");
	}
	if (root) {
		xml_tag* curlog = xml_findfirst(root, "groundspeak:log");
		if (curlog) {
			gbfputs("\nLOG:\n", fd);
		}
		for (; curlog; curlog = xml_findnext(root, curlog, "groundspeak:log")) {
			xml_tag* logpart = xml_findfirst(curlog, "groundspeak:type");
			if (logpart) {
				gbfprintf(fd, "%s\n", logpart->cdata);
			}
			logpart = xml_findfirst(curlog, "groundspeak:date");
			if (logpart) {
				time_t logtime = xml_parse_time(logpart->cdata, NULL);
				const struct tm* logtm = gmtime(&logtime);
				gbfprintf(fd, "%d-%d-%d ", logtm->tm_year + 1900, logtm->tm_mon + 1, logtm->tm_mday);
			}
			logpart = xml_findfirst(curlog, "groundspeak:finder");
			if (logpart) {
				char* s = cet_str_utf8_to_any(logpart->cdata, global_opts.charset);
				gbfputs(s, fd);
				xfree(s);
			}
			logpart = xml_findfirst(curlog, "groundspeak:text");
			if (logpart) {
				char* s = cet_str_utf8_to_any(logpart->cdata, global_opts.charset);
				gbfprintf(fd, ", %s", s);
				xfree(s);
			}
			gbfputc('\n', fd);
		}
	}
	gbfputc(0, fd);
	*notes_size = fd->memlen;
	*notes = xmalloc(*notes_size);
	memcpy(*notes, fd->handle.mem, *notes_size);
	gbfclose(fd);
}

static void
write_waypoint_notes(const char* notes, unsigned size, const char* name)
{
	message_t m;
	const unsigned name_size = strlen(name) + 1;
	const unsigned bytes_per_msg = (10 * (delbin_os_packet_size - 2)) - name_size - 20;
	const unsigned msg_count = (size + (bytes_per_msg - 1)) / bytes_per_msg;
	unsigned i = 1;

	do {
		char* pp;
		unsigned n = bytes_per_msg;
		msg_waypoint_note_t* p;
		message_init_size(&m, 2 + 2 + 1 + name_size + 2 + bytes_per_msg);
		p = m.data;
		le_write16(p->index, i++);
		le_write16(p->total, msg_count);
		p->name_size = name_size;
		memcpy(p->name, name, p->name_size);
		pp = p->name + p->name_size;
		if (n > size) {
			n = size;
		}
		le_write16(pp, n);
		pp += 2;
		memcpy(pp, notes, n);
		pp += n;
		if (*(pp - 1)) {
			*pp++ = 0;
		}
		notes += n;
		size -= n;
		m.size = pp - (char*)p;
		add_to_batch(MSG_WAYPOINT_NOTE_IN, &m);
	} while (size != 0);
}

static void
write_waypoint(const waypoint* wp)
{
	message_t m;
	msg_waypoint_t* p;
	const char* name = wp->shortname;
	unsigned name_size;
	char* notes;
	unsigned notes_size = 0;
	unsigned extended_notes_size = 0;
	char* notes_freeable = NULL;
	int symbol = -1;
	float elev = UNKNOWN_ELEV;
	char* pp;

	if (waypt_empty_gc_data(wp)) {
		notes = wp->notes;
		if (notes == NULL && wp->description && strcmp(wp->shortname, wp->description)) {
			notes = wp->description;
		}
		if (notes) {
			notes_size = strlen(notes) + 1;
		}
	} else {
		get_gc_notes(wp, &symbol, &notes, &notes_size);
		notes_freeable = notes;
		if (wp->description) {
			name = wp->description;
		}
	}
	if (notes_size > 800) {
		if (use_extended_notes) {
			extended_notes_size = notes_size;
			notes_size = 1;
		} else {
			notes_size = 800;
		}
	}
	name_size = strlen(name) + 1;
	if (name_size > 255) {
		name_size = 255;
	}
	message_init_size(&m, 31 + name_size + notes_size);
	p = m.data;

	waypoint_i++;
	le_write32(p->total, waypoint_n);
	le_write32(p->index, waypoint_i);
	encode_time(wp->creation_time, &p->year);
	le_write32(p->latitude, delbin_deg2rad(wp->latitude));
	le_write32(p->longitude, delbin_deg2rad(wp->longitude));
	if (wp->altitude > unknown_alt) {
		elev = wp->altitude;
	}
	le_write_float(p->elevation, elev);
	if (symbol < 0) {
		symbol = 0;
		if (wp->icon_descr) {
			symbol = waypoint_symbol_index(wp->icon_descr);
		}
	}
	p->symbol = symbol;
	p->name_size = name_size;
	memcpy(p->name, name, name_size - 1);
	p->name[name_size - 1] = 0;
	pp = p->name + name_size;
	m.size = (pp + 2 + notes_size) - (char*)p;
	if (extended_notes_size) {
		le_write16(pp, 0xffff);
		pp[2] = 0;
	} else {
		le_write16(pp, notes_size);
		if (notes) {
			memcpy(pp + 2, notes, notes_size - 1);
			pp[2 + notes_size - 1] = 0;
		}
	}

	add_to_batch(MSG_WAYPOINT_IN, &m);

	if (extended_notes_size) {
		write_waypoint_notes(notes, extended_notes_size, name);
	}
	if (notes_freeable) {
		xfree(notes_freeable);
	}
	if (global_opts.debug_level >= DBGLVL_L)
		warning(MYNAME ": wrote waypoint %u '%s'\n", waypoint_i, name);
}

static void
write_waypoints(void)
{
	waypoint_i = 0;
	waypoint_n = waypt_count();
	waypt_disp_all(write_waypoint);
	send_batch(TRUE);
}

//-----------------------------------------------------------------------------
// Track reading

static void
decode_sat_fix(waypoint* wp, const gbuint8 status)
{
	switch (status & 3) {
	case 1: wp->fix = fix_none; break;
	case 2: wp->fix = fix_2d; break;
	case 3:
		wp->fix = fix_3d;
		if (status & 4) {
			wp->fix = fix_dgps;
		}
		break;
	}
}

static void
decode_track_point(const void* data, unsigned* wp_array_i, unsigned max_point)
{
	const msg_track_point_t* p = data;
	const unsigned n = p->number;
	unsigned i;
	unsigned j = *wp_array_i;

	if (j + n > max_point) {
		fatal(MYNAME ": read too many track points\n");
	}
	for (i = 0; i < n; i++, j++) {
		waypoint* wp = waypt_new();
		float elev = le_read_float(p->point[i].elevation);
		wp_array[j] = wp;
		wp->creation_time = decode_time(&p->point[i].year);
		wp->latitude = delbin_rad2deg(le_read32(p->point[i].latitude));
		wp->longitude = delbin_rad2deg(le_read32(p->point[i].longitude));
		if (elev > UNKNOWN_ELEV) {
			wp->altitude = elev;
		}
		wp->speed = le_readu16(p->point[i].speed);
		wp->speed *= (100.0f / (60 * 60));
		wp->wpt_flags.speed = 1;
		decode_sat_fix(wp, p->point[i].status);
		// use microseconds as track segment marker
		wp->microseconds = p->point[i].status & 0x10;
	}
	*wp_array_i = j;
}

static void
read_track(route_head* track)
{
	message_t m;
	message_t* msg_array;
	const msg_track_header_t* p;
	unsigned msg_array_n;
	unsigned wp_array_i = 0;
	unsigned n_point;
	unsigned segment = 1;
	char* track_name = NULL;
	unsigned i;
	int attempt = ATTEMPT_MAX;

	message_init(&m);
	// read track messages
	for (;;) {
		m.size = MSG_REQUEST_TRACKS_SIZE;
		memset(m.data, 0, m.size);
		((char*)m.data)[0] = 1;  // Download single track
		strcpy((char*)m.data + 1, track->rte_name);
		message_write(MSG_REQUEST_TRACKS, &m);
		if (get_batch(&msg_array, &msg_array_n))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading track '%s' failed\n", track->rte_name);
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": timed out reading track '%s', retrying\n", track->rte_name);
		m.size = MSG_BREAK_SIZE;
		memset(m.data, 0, m.size);
		message_write(MSG_BREAK, &m);
	}
	message_free(&m);
	if (msg_array_n == 0 || message_get_id(&msg_array[0]) != MSG_TRACK_HEADER_OUT) {
		fatal(MYNAME ": reading track '%s' failed (missing track header)\n", track->rte_name);
	}
	// process track messages
	p = msg_array[0].data;
	if (le_readu16(p->comment_size)) {
		track->rte_desc = xstrdup(p->comment);
	}
	track->line_color.bbggrr = track_color(p->color[0]);
	n_point = le_readu32(p->total_points);
	wp_array = xcalloc(n_point, sizeof(*wp_array));
	message_free(&msg_array[0]);
	for (i = 1; i < msg_array_n; i++) {
		unsigned id = message_get_id(&msg_array[i]);
		if (id == MSG_TRACK_POINT_OUT) {
			decode_track_point(msg_array[i].data, &wp_array_i, n_point);
		} else {
			fatal(MYNAME ": unexpected message %x while reading track '%s'\n", id, track->rte_name);
		}
		message_free(&msg_array[i]);
	}
	xfree(msg_array);
	if (n_point != wp_array_i) {
		fatal(MYNAME ": track point count mismatch, expected %u, got %u\n", n_point, wp_array_i);
	}
	if (global_opts.debug_level >= DBGLVL_L)
		warning(MYNAME ": read track '%s' %u points\n", track->rte_name, n_point);
	// make track (one for each segment)
	track_add_wpt(track, wp_array[0]);
	for (i = 1; i < n_point; i++) {
		// if track segment marker
		if (wp_array[i]->microseconds) {
			const char* desc = track->rte_desc;
			if (track_name == NULL) {
				// save original name, append " #1" to first segment name
				track_name = track->rte_name;
				track->rte_name = xmalloc(strlen(track_name) + 4);
				sprintf(track->rte_name, "%s #1", track_name);
			}
			// make a new track for segment
			segment++;
			track_add_head(track);
			track = route_head_alloc();
			track->rte_name = xmalloc(strlen(track_name) + 7);
			sprintf(track->rte_name, "%s #%u", track_name, segment);
			track->rte_desc = xstrdup(desc);
		}
		wp_array[i]->microseconds = 0;
		track_add_wpt(track, wp_array[i]);
	}
	track_add_head(track);
	if (track_name) {
		xfree(track_name);
	}
	xfree(wp_array);
}

static void
read_tracks(void)
{
	message_t m;
	message_t* msg_array;
	unsigned msg_array_n;
	route_head** track_array;
	unsigned total;
	unsigned i;
	int attempt = ATTEMPT_MAX;

	message_init(&m);
	// get number of tracks
	for (;;) {
		m.size = 0;
		message_write(MSG_TRACK_COUNT, &m);
		if (message_read(MSG_TRACK_COUNT, &m))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading track count failed\n");
	}
	total = le_readu32(m.data);
	if (global_opts.debug_level >= DBGLVL_L)
		warning(MYNAME ": %u tracks\n", total);
	if (total == 0) {
		message_free(&m);
		return;
	}

	// First get track headers, then request each track with non-zero number of points
	attempt = ATTEMPT_MAX;
	for (;;) {
		m.size = MSG_REQUEST_TRACKS_SIZE;
		memset(m.data, 0, m.size);
		((char*)m.data)[0] = 2;  // Download all track headers
		message_write(MSG_REQUEST_TRACKS, &m);
		if (get_batch(&msg_array, &msg_array_n))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading track headers failed\n");
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": timed out reading track headers, retrying\n");
		m.size = MSG_BREAK_SIZE;
		memset(m.data, 0, m.size);
		message_write(MSG_BREAK, &m);
	}
	message_free(&m);
	track_array = xcalloc(total, sizeof(*track_array));
	for (i = 0; i < msg_array_n; i++) {
		unsigned id = message_get_id(&msg_array[i]);
		if (id == MSG_TRACK_HEADER_OUT) {
			const msg_track_header_t* p = msg_array[i].data;
			if (le_readu32(p->total_points)) {
				track_array[i] = route_head_alloc();
				track_array[i]->rte_name = xstrdup(p->name);
			}
		} else {
			fatal(MYNAME ": unexpected message %x while reading track headers\n", id);
		}
		message_free(&msg_array[i]);
	}
	xfree(msg_array);
	// get each track
	for (i = 0; i < total; i++) {
		if (track_array[i]) {
			read_track(track_array[i]);
		}
	}
	xfree(track_array);
}

//-----------------------------------------------------------------------------
// Track writing

static void
write_track_points(void)
{
	message_t m;
	const unsigned pt_per_msg = 10;
	msg_track_point_t* p = NULL;
	unsigned i = 0;
	unsigned j = 0;

	do {
		const waypoint* wp = wp_array[i];
		float f;

		if (j == 0) {
			message_init_size(&m, 9 + 23 * pt_per_msg);
			p = m.data;
			le_write32(p->total, waypoint_n);
			le_write32(p->index, i + 1);
		}
		assert(p);
		encode_time(wp->creation_time, &p->point[j].year);
		le_write32(p->point[j].latitude, delbin_deg2rad(wp->latitude));
		le_write32(p->point[j].longitude, delbin_deg2rad(wp->longitude));
		f = UNKNOWN_ELEV;
		if (wp->altitude > unknown_alt) {
			f = wp->altitude;
		}
		le_write_float(p->point[j].elevation, f);
		f = WAYPT_GET(wp, speed, 0);
		f *= (60 * 60) / 100;
		le_write16(p->point[j].speed, (gbuint16)f);
		f = WAYPT_GET(wp, course, 0);
		f *= 100;
		le_write16(p->point[j].heading, (gbuint16)f);
		switch (wp->fix) {
		default:       p->point[j].status = 0; break;
		case fix_none: p->point[j].status = 1; break;
		case fix_2d:   p->point[j].status = 2; break;
		case fix_3d:   p->point[j].status = 3; break;
		case fix_dgps: p->point[j].status = 4 | 3; break;
		}
		i++;
		j++;
		if (j == pt_per_msg || i == waypoint_n) {
			p->number = j;
			m.size = 9 + 23 * j;
			add_to_batch(MSG_TRACK_POINT_IN, &m);
			j = 0;
		}
	} while (i < waypoint_n);
}

static void
write_track_begin(const route_head* track)
{
	waypoint_i = 0;
	waypoint_n = track->rte_waypt_ct;
	if (waypoint_n) {
		wp_array = xmalloc(waypoint_n * sizeof(*wp_array));
	}
}

static void
write_track_point(const waypoint* wp)
{
	wp_array[waypoint_i++] = (waypoint*)wp;
}

static void
write_track_end(const route_head* track)
{
	message_t m;
	msg_track_header_in_t* p;
	unsigned comment_size = 0;

	if (waypoint_n == 0) {
		return;
	}
	if (track->rte_desc) {
		comment_size = strlen(track->rte_desc) + 1;
	}
	message_init_size(&m, sizeof(msg_track_header_in_t) - 1 + comment_size);
	p = m.data;
	memset(p->name, 0, sizeof(p->name));
	if (track->rte_name) {
		strncpy(p->name, track->rte_name, sizeof(p->name) - 1);
	} else {
		sprintf(p->name, "%lu", (long)wp_array[0]->creation_time);
	}
	le_write32(p->total_points, waypoint_n);
	encode_time(current_time(), &p->year);
	le_write16(p->color, track_color_index(track->line_color.bbggrr));
	le_write16(p->comment_size, comment_size);
	if (comment_size) {
		memcpy(p->comment, track->rte_desc, comment_size);
	}
	add_to_batch(MSG_TRACK_HEADER_IN, &m);
	write_track_points();
	send_batch(FALSE);
	xfree(wp_array);
}

static void
write_tracks(void)
{
	track_disp_all(write_track_begin, write_track_end, write_track_point);
}

//-----------------------------------------------------------------------------
// Route reading

static void
decode_route_shape(const void* data, unsigned* wp_array_i)
{
	const msg_route_shape_t* p = data;
	const unsigned n = p->number;
	unsigned i;
	unsigned j = *wp_array_i;

	for (i = 0; i < n; i++, j++) {
		char buf[32];
		waypoint* wp = waypt_new();
		wp_array[j] = wp;
		wp->latitude = delbin_rad2deg(le_read32(p->point[i].latitude));
		wp->longitude = delbin_rad2deg(le_read32(p->point[i].longitude));
		sprintf(buf, "SHP%03u", j);
		wp->shortname = xstrdup(buf);
	}
	*wp_array_i = j;
}

static waypoint*
decode_route_point(const void* data)
{
	const msg_route_point_t* p = data;
	const char* s = NULL;
	gbfile* fd = gbfopen(NULL, "w", MYNAME);
	waypoint* wp = waypt_new();
	if (p->name[0]) {
		wp->shortname = xstrdup(p->name);
	}
	// give these a higher priority than the shape points
	wp->route_priority = 1;
	wp->latitude = delbin_rad2deg(le_read32(p->latitude));
	wp->longitude = delbin_rad2deg(le_read32(p->longitude));
	switch (p->itinerary_type) {
	case 1: s = "Start"; break;
	case 2: s = "Stop"; break;
	case 3: s = "Finish"; break;
	case 4: s = "Via"; break;
	case 5: s = "Via Hidden"; break;
	case 6:
		switch (p->turn_type) {
		case 1: s = "Turn, Straight"; break;
		case 2: s = "Turn, Right"; break;
		case 3: s = "Turn, Bear Right"; break;
		case 4: s = "Turn, Keep Right"; break;
		case 5: s = "Turn, Left"; break;
		case 6: s = "Turn, Bear Left"; break;
		case 7: s = "Turn, Keep Left"; break;
		case 8: s = "Turn, Reverse Direction"; break;
		case 9: s = "Turn, Street Name Change"; break;
		}
		break;
	}
	if (s) {
		gbfprintf(fd, "Type: %s", s);
	}
	if (p->exit_label_size && p->exit_label[0]) {
		gbfprintf(fd, "\nExit: %s", p->exit_label);
	}
	s = p->exit_label + p->exit_label_size;
	if (s[0] && s[1]) {
		gbfprintf(fd, "\n%s", s + 1);
	}
	if (fd->memlen) {
		gbfputc(0, fd);
		wp->notes = xmalloc(fd->memlen);
		memcpy(wp->notes, fd->handle.mem, fd->memlen);
	}
	gbfclose(fd);
	return wp;
}

static void
read_route(route_head* route)
{
	message_t m;
	message_t* msg_array;
	const msg_route_header_t* p;
	unsigned msg_array_n;
	unsigned wp_array_i = 0;
	unsigned route_total, shape_total, total;
	unsigned i;
	int attempt = ATTEMPT_MAX;

	message_init(&m);
	for (;;) {
		m.size = MSG_REQUEST_ROUTES_SIZE;
		memset(m.data, 0, m.size);
		((char*)m.data)[0] = 1;  // Download single route
		strcpy((char*)m.data + 1, route->rte_name);
		message_write(MSG_REQUEST_ROUTES, &m);
		if (get_batch(&msg_array, &msg_array_n))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading route '%s' failed (timed out)\n", route->rte_name);
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": timed out reading route route '%s', retrying\n", route->rte_name);
		m.size = MSG_BREAK_SIZE;
		memset(m.data, 0, m.size);
		message_write(MSG_BREAK, &m);
	}
	message_free(&m);
	if (msg_array_n == 0 || message_get_id(&msg_array[0]) != MSG_ROUTE_HEADER_OUT) {
		fatal(MYNAME ": missing route header\n");
	}
	p = msg_array[0].data;
	route_total = le_readu32(p->total_route_point);
	shape_total = le_readu32(p->total_shape_point);
	total = route_total + shape_total;
	wp_array = xcalloc(total, sizeof(*wp_array));
	if (global_opts.debug_level >= DBGLVL_L) {
		warning(MYNAME ": route '%s' %u points, %u shape points\n",
			route->rte_name, route_total, shape_total);
	}
	message_free(&msg_array[0]);
	for (i = 1; i < msg_array_n; i++) {
		unsigned id = message_get_id(&msg_array[i]);
		if (id == MSG_ROUTE_POINT_OUT) {
			wp_array[wp_array_i] = decode_route_point(msg_array[i].data);
			if (global_opts.debug_level >= DBGLVL_L)
				warning(MYNAME ": route point '%s'\n", wp_array[wp_array_i]->shortname);
			wp_array_i++;
		} else if (id == MSG_ROUTE_SHAPE_OUT) {
			decode_route_shape(msg_array[i].data, &wp_array_i);
		} else {
			fatal(MYNAME ": unexpected message %x while reading route '%s'\n", id, route->rte_name);
		}
		message_free(&msg_array[i]);
	}
	xfree(msg_array);
	if (total != wp_array_i) {
		fatal(MYNAME ": route point count mismatch, expected %u, got %u\n", total, wp_array_i);
	}
	for (i = 0; i < total; i++) {
		route_add_wpt(route, wp_array[i]);
	}
	xfree(wp_array);
	route_add_head(route);
}

static void
read_routes(void)
{
	message_t m;
	message_t* msg_array;
	unsigned msg_array_n;
	route_head** route_array;
	unsigned total;
	unsigned i;
	int attempt = ATTEMPT_MAX;

	message_init(&m);
	// get number of routes
	for (;;) {
		m.size = 0;
		message_write(MSG_ROUTE_COUNT, &m);
		if (message_read(MSG_ROUTE_COUNT, &m))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading route count failed\n");
	}
	total = le_readu32(m.data);
	if (global_opts.debug_level >= DBGLVL_L)
		warning(MYNAME ": %u routes\n", total);
	if (total == 0) {
		message_free(&m);
		return;
	}

	// First get route headers, then request each route
	attempt = ATTEMPT_MAX;
	for (;;) {
		m.size = MSG_REQUEST_ROUTES_SIZE;
		memset(m.data, 0, m.size);
		((char*)m.data)[0] = 2;  // Download all route headers
		message_write(MSG_REQUEST_ROUTES, &m);
		if (get_batch(&msg_array, &msg_array_n))
			break;
		if (--attempt == 0)
			fatal(MYNAME ": reading route headers failed\n");
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": timed out reading route headers, retrying\n");
		m.size = MSG_BREAK_SIZE;
		memset(m.data, 0, m.size);
		message_write(MSG_BREAK, &m);
	}
	message_free(&m);
	route_array = xcalloc(total, sizeof(*route_array));
	for (i = 0; i < msg_array_n; i++) {
		unsigned id = message_get_id(&msg_array[i]);
		if (id == MSG_ROUTE_HEADER_OUT) {
			route_array[i] = route_head_alloc();
			route_array[i]->rte_name = xstrdup(((msg_route_header_t*)msg_array[i].data)->name);
		} else {
			fatal(MYNAME ": unexpected message %x while reading route headers\n", id);
		}
		message_free(&msg_array[i]);
	}
	xfree(msg_array);
	// get each route
	for (i = 0; i < total; i++) {
		read_route(route_array[i]);
	}
	xfree(route_array);
}

//-----------------------------------------------------------------------------
// Route writing

static unsigned route_point_n;
static unsigned shape_point_n;
static unsigned* shape_point_counts;

static void
write_route_shape_points(waypoint** array, unsigned n)
{
	message_t m;
	const unsigned pt_per_msg = 25;
	msg_route_shape_t* p = NULL;
	unsigned i = 0;
	unsigned j = 0;

	do {
		if (j == 0) {
			message_init_size(&m, 10 + 8 * pt_per_msg);
			p = m.data;
			le_write32(p->total, n);
			le_write32(p->index, i + 1);
			p->reserved = 0;
		}
		assert(p);
		le_write32(p->point[j].latitude, delbin_deg2rad(array[i]->latitude));
		le_write32(p->point[j].longitude, delbin_deg2rad(array[i]->longitude));
		i++;
		j++;
		if (j == pt_per_msg || i == n) {
			p->number = j;
			m.size = 10 + 8 * j;
			add_to_batch(MSG_ROUTE_SHAPE_IN, &m);
			j = 0;
		}
	} while (i < n);
}

static void
write_route_points(void)
{
	unsigned route_point_i = 0;
	unsigned i = 0;

	while (i < waypoint_n) {
		message_t m;
		unsigned shape_n;
		const waypoint* wp = wp_array[i];
		msg_route_point_t* p;
		char* s;

		message_init_size(&m, sizeof(msg_route_point_t) + 1 + 1 + 4);
		p = m.data;
		memset(m.data, 0, m.size);
		route_point_i++;
		shape_n = shape_point_counts[route_point_i];
		le_write32(p->total, route_point_n);
		le_write32(p->index, route_point_i);
		if (wp->shortname) {
			strncpy(p->name, wp->shortname, sizeof(p->name) - 1);
		} else {
			sprintf(p->name, "RPT%u", route_point_i);
		}
		le_write32(p->latitude, delbin_deg2rad(wp->latitude));
		le_write32(p->longitude, delbin_deg2rad(wp->longitude));
		p->exit_label_size = 1;
		s = p->exit_label + p->exit_label_size;
		s[0] = 1;  // comment size
		le_write32(s + 2, shape_n);
		if (route_point_i == 1) {
			p->itinerary_type = 1; // start
		} else if (route_point_i == route_point_n) {
			p->itinerary_type = 3; // finish
		}
		add_to_batch(MSG_ROUTE_POINT_IN, &m);
		i++;
		if (shape_n) {
			write_route_shape_points(&wp_array[i], shape_n);
			i += shape_n;
		}
	}
}

static void
write_route_begin(const route_head* track)
{
	waypoint_i = 0;
	route_point_n = 0;
	shape_point_n = 0;
	waypoint_n = track->rte_waypt_ct;
	if (waypoint_n) {
		wp_array = xmalloc(waypoint_n * sizeof(*wp_array));
		shape_point_counts = xcalloc(waypoint_n, sizeof(*shape_point_counts));
	}
}

static void
write_route_point(const waypoint* wp)
{
	const char* s = wp->shortname;
	wp_array[waypoint_i++] = (waypoint*)wp;
	if (s && s[0] == 'S' && s[1] == 'H' && s[2] == 'P' && s[3] >= '0' && s[3] <= '9') {
		shape_point_n++;
		shape_point_counts[route_point_n]++;
	} else {
		route_point_n++;
	}
}

static void
write_route_end(const route_head* route)
{
	message_t m;
	msg_route_header_in_t* p;

	if (waypoint_n == 0) {
		return;
	}
	message_init_size(&m, sizeof(msg_route_header_in_t));
	p = m.data;
	memset(p->name, 0, sizeof(p->name));
	if (route->rte_name) {
		strncpy(p->name, route->rte_name, sizeof(p->name) - 1);
	} else {
		sprintf(p->name, "%lu", (long)wp_array[0]->creation_time);
	}
	p->type = 0;
	le_write32(p->total_route_point, route_point_n);
	le_write32(p->total_shape_point, shape_point_n);
	add_to_batch(MSG_ROUTE_HEADER_IN, &m);
	write_route_points();
	send_batch(TRUE);
	if (wp_array) {
		xfree(wp_array);
		xfree(shape_point_counts);
	}
}

static void
write_routes(void)
{
	route_disp_all(write_route_begin, write_route_end, write_route_point);
}

//-----------------------------------------------------------------------------
// Current position

static waypoint*
decode_navmsg(const void* data)
{
	waypoint* wp = waypt_new();
	const msg_navigation_t* p = data;
	struct tm t;

	t.tm_year = le_readu16(p->year) - 1900;
	t.tm_mon = p->month - 1;
	t.tm_mday = p->day;
	t.tm_hour = p->hour;
	t.tm_min = p->minute;
	t.tm_sec = p->second;
	wp->creation_time = mkgmtime(&t);
	wp->sat = p->satellites;
	wp->latitude = le_read_double(p->latitude);
	wp->longitude = le_read_double(p->longitude);
	wp->altitude = le_read_double(p->elevation);
	wp->speed = le_read_float(p->speed);
	wp->speed *= (1000.0f / (60 * 60));
	wp->wpt_flags.speed = 1;
	wp->course = le_readu16(p->heading);
	wp->course /= 100;
	wp->wpt_flags.course = 1;
	decode_sat_fix(wp, p->fix_status);
	wp->shortname = xstrdup("Position");
	return wp;
}

static waypoint*
read_position(void)
{
	waypoint* wp;
	message_t m;

	message_init(&m);
	message_read(MSG_NAVIGATION, &m);
	wp = decode_navmsg(m.data);
	if (wp->fix > fix_none &&
		message_read_1(MSG_SATELLITE_INFO, &m) == MSG_SATELLITE_INFO)
	{
		const msg_satellite_t* p = m.data;
		wp->hdop = le_readu16(p->hdop);
		wp->hdop /= 100;
		wp->vdop = le_readu16(p->vdop);
		wp->vdop /= 100;
		wp->pdop = le_readu16(p->pdop);
		wp->pdop /= 100;
	}
	message_free(&m);
	return wp;
}

//-----------------------------------------------------------------------------

static void
delbin_list_units()
{
	int i;
	for (i = 0; i < n_delbin_units; i++) {
		printf("%d %s %s\n", 
			delbin_unit_info[i].unit_number,
			delbin_unit_info[i].unit_serial_number,
			delbin_unit_info[i].unit_name );
	}
}



static void
delbin_rw_init(const char *fname)
{
	message_t m;

	delbin_os_ops.init(fname);

	// Send a break to clear any state from a previous failure
	message_init(&m);
	m.size = MSG_BREAK_SIZE;
	memset(m.data, 0, m.size);
	message_write(MSG_BREAK, &m);
	// get version info
	m.size = 0;
	message_write(MSG_VERSION, &m);
	if (message_read(MSG_VERSION, &m)) {
		const msg_version_t* p = m.data;
		if (global_opts.debug_level >= DBGLVL_L)
			warning(MYNAME ": device %s %s\n", p->product, p->firmware);
		if (opt_long_notes) {
			use_extended_notes = TRUE;
		} else if (strstr(p->product, "PN-20")) {
			use_extended_notes = p->firmware[0] > '1' ||
				(p->firmware[0] == '1' && p->firmware[2] >= '6');
		} else if (strstr(p->product, "PN-40")) {
			use_extended_notes = p->firmware[0] > '2' ||
				(p->firmware[0] == '2' && p->firmware[2] >= '5');
		}
		delbin_unit_info[n_delbin_units].unit_number = n_delbin_units;
		delbin_unit_info[n_delbin_units].unit_serial_number = xstrndup(p->serial, sizeof(p->serial));
		delbin_unit_info[n_delbin_units].unit_name = xstrndup(p->product, sizeof(p->product));
		n_delbin_units++;
	}
	message_free(&m);

	if (strlen(fname) > 4) {
		if (0 == strcmp(fname+4, "list")) {
			delbin_list_units();
			exit(1);
		}
	}
}

static void
delbin_rw_deinit(void)
{
	delbin_os_ops.deinit();
}

static void
delbin_read(void)
{
	if (doing_wpts) {
		if (opt_getposn) {
			waypt_add(read_position());
		} else {
			read_waypoints();
		}
	}
	if (doing_trks) {
		read_tracks();
	}
	if (doing_rtes) {
		read_routes();
	}
}

static void
delbin_write(void)
{
	if (doing_wpts) {
		write_waypoints();
	}
	if (doing_trks) {
		write_tracks();
	}
	if (doing_rtes) {
		write_routes();
	}
}

static waypoint*
delbin_rd_position(posn_status* status)
{
	return read_position();
}

ff_vecs_t delbin_vecs = {
	ff_type_serial,
	FF_CAP_RW_ALL,
	delbin_rw_init,	
	delbin_rw_init,
	delbin_rw_deinit,	
	delbin_rw_deinit,
	delbin_read,
	delbin_write,
	NULL,
	delbin_args,
	CET_CHARSET_LATIN1, 1,
	{ delbin_rw_init, delbin_rd_position, delbin_rw_deinit }
};

//=============================================================================
// OS device I/O implementations

#define VENDOR_ID 0x1163
#define PRODUCT_ID 0x2020

//-----------------------------------------------------------------------------
// Windows
#if _WIN32

#undef HAVE_LIBUSB

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <setupapi.h>
#include <hidsdi.h>

static HANDLE hid_handle;

static void
win_os_init(const char* fname)
{
	GUID hid_guid;
	HDEVINFO dev_info;
	SP_DEVICE_INTERFACE_DATA dev_int_data;
	PHIDP_PREPARSED_DATA hid_ppd;
	HIDP_CAPS hid_caps;
	const char* busy = "";
	unsigned i;

	hid_handle = INVALID_HANDLE_VALUE;
	HidD_GetHidGuid(&hid_guid);
	dev_info = SetupDiGetClassDevs(&hid_guid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
	if (dev_info == INVALID_HANDLE_VALUE) {
		fatal(MYNAME ": SetupDiGetClassDevs failed %u\n", GetLastError());
	}
	dev_int_data.cbSize = sizeof(dev_int_data);
	for (i = 0; SetupDiEnumDeviceInterfaces(dev_info, NULL, &hid_guid, i, &dev_int_data); i++) {
		union {
			SP_DEVICE_INTERFACE_DETAIL_DATA detail_data;
			char buf[300];
		} u;
		u.detail_data.cbSize = sizeof(u.detail_data);
		if (SetupDiGetDeviceInterfaceDetail(dev_info,
		    &dev_int_data, &u.detail_data, sizeof(u.buf), NULL, NULL))
		{
			HANDLE h = CreateFile(u.detail_data.DevicePath,
				FILE_READ_DATA | FILE_WRITE_DATA, 0, NULL, OPEN_EXISTING, 0, NULL);
			if (h != INVALID_HANDLE_VALUE) {
				HIDD_ATTRIBUTES hid_attr;
				hid_attr.Size = sizeof(hid_attr);
				if (HidD_GetAttributes(h, &hid_attr) &&
				    hid_attr.VendorID == VENDOR_ID && hid_attr.ProductID == PRODUCT_ID)
				{
					hid_handle = h;
					break;
				}
				CloseHandle(h);
			} else if (GetLastError() == ERROR_SHARING_VIOLATION &&
				strstr(u.detail_data.DevicePath, "1163") &&
				strstr(u.detail_data.DevicePath, "2020"))
			{
				busy = " (device busy?)";
			}
		}
	}
	SetupDiDestroyDeviceInfoList(dev_info);
	if (hid_handle == INVALID_HANDLE_VALUE) {
		fatal(MYNAME ": no DeLorme PN found%s\n", busy);
	}
	if (!HidD_GetPreparsedData(hid_handle, &hid_ppd)) {
		fatal(MYNAME ": HidD_GetPreparsedData failed %u\n", GetLastError());
	}
	if (!HidP_GetCaps(hid_ppd, &hid_caps)) {
		fatal(MYNAME ": HidP_GetCaps failed %u\n", GetLastError());
	}
	// report length includes report id
	delbin_os_packet_size = hid_caps.InputReportByteLength - 1;
	HidD_FreePreparsedData(hid_ppd);
}

static void
win_os_deinit(void)
{
	CloseHandle(hid_handle);
}

static unsigned
win_os_packet_read(void* buf)
{
	unsigned n;
	char buf1[257];
	// first byte is report id
	if (ReadFile(hid_handle, buf1, delbin_os_packet_size + 1, &n, NULL) == 0) {
		fatal(MYNAME ": ReadFile failed %u\n", GetLastError());
	}
	if (n > 0) {
		n--;
	}
	memcpy(buf, buf1 + 1, n);
	return n;
}

static unsigned
win_os_packet_write(const void* buf, unsigned size)
{
	unsigned n;
	char buf1[257];
	// first byte is report id
	buf1[0] = 0;
	memcpy(buf1 + 1, buf, size);
	if (WriteFile(hid_handle, buf1, delbin_os_packet_size + 1, &n, NULL) == 0) {
		fatal(MYNAME ": WriteFile failed %u\n", GetLastError());
	}
	if (n > size) {
		n = size;
	}
	return n;
}

delbin_os_ops_t delbin_os_ops = {
	win_os_init,
	win_os_deinit,
	win_os_packet_read,
	win_os_packet_write
};

#endif // _WIN32

//-----------------------------------------------------------------------------
// MacOS X
#if __APPLE__

#undef HAVE_LIBUSB

#include <pthread.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/hid/IOHIDLib.h>

// IOHIDDeviceInterface121::getReport() does not work, it hangs the process
// in some sort of unkillable state.  So reading is done via a separate thread
// with a run loop and interrupt report callback. Yuck.

static IOHIDDeviceInterface122** device;
static pthread_t thread;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static char* report_buf;
static char* packet_array[32];
static unsigned packet_array_head;
static unsigned packet_array_tail;
static CFRunLoopRef run_loop;

static void*
thread_func(void* run_loop_source)
{
	run_loop = CFRunLoopGetCurrent();
	CFRunLoopAddSource(run_loop, run_loop_source, kCFRunLoopDefaultMode);
	CFRunLoopRun();
	return NULL;
}

static void
interrupt_report_cb(void* target, IOReturn result, void* refcon, void* sender, UInt32 bufferSize)
{
	memcpy(packet_array[packet_array_head], report_buf, delbin_os_packet_size);
	pthread_mutex_lock(&mutex);
	if (packet_array_head == packet_array_tail) {
		pthread_cond_signal(&cond);
	}
	packet_array_head++;
	packet_array_head &= sizeofarray(packet_array) - 1;
	if (packet_array_head == packet_array_tail && global_opts.debug_level >= DBGLVL_M)
		warning(MYNAME ": packet_array overrun, packets lost\n");
	pthread_mutex_unlock(&mutex);
}

static void
mac_os_init(const char* fname)
{
	CFMutableDictionaryRef dict = IOServiceMatching(kIOHIDDeviceKey);
	io_service_t service;
	IOCFPlugInInterface** plugin;
	CFNumberRef cf_num;
	CFRunLoopSourceRef run_loop_source;
	int i;
	kern_return_t kr;
	HRESULT hr;
	IOReturn ir;
	SInt32 unused;

	i = VENDOR_ID;
	cf_num = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &i);
	CFDictionaryAddValue(dict, CFSTR(kIOHIDVendorIDKey), cf_num);
	CFRelease(cf_num);
	i = PRODUCT_ID;
	cf_num = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &i);
	CFDictionaryAddValue(dict, CFSTR(kIOHIDProductIDKey), cf_num);
	CFRelease(cf_num);
	service = IOServiceGetMatchingService(kIOMasterPortDefault, dict);
	if (service == 0) {
		fatal(MYNAME ": no DeLorme PN found\n");
	}
	kr = IOCreatePlugInInterfaceForService(
		service, kIOHIDDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin, &unused);
	if (kr)
		fatal(MYNAME ": IOCreatePlugInInterfaceForService failed 0x%x\n", (int)kr);
	IOObjectRelease(service);
	hr = (*plugin)->QueryInterface(plugin, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID122), (void*)&device);
	if (hr)
		fatal(MYNAME ": QueryInterface failed 0x%x\n", (int)hr);
	(*plugin)->Release(plugin);
	ir = (*device)->open(device, kIOHIDOptionsTypeSeizeDevice);
	if (ir)
		fatal(MYNAME ": device open failed 0x%x\n", (int)ir);
	ir = (*device)->createAsyncEventSource(device, &run_loop_source);
	if (ir)
		fatal(MYNAME ": createAsyncEventSource failed 0x%x\n", (int)ir);
	delbin_os_packet_size = 64;
	report_buf = xmalloc(delbin_os_packet_size);
	for (i = sizeofarray(packet_array); i--;) {
		packet_array[i] = xmalloc(delbin_os_packet_size);
	}
	ir = (*device)->setInterruptReportHandlerCallback(
		device, report_buf, delbin_os_packet_size, interrupt_report_cb, NULL, NULL);
	if (ir)
		fatal(MYNAME ": setInterruptReportHandlerCallback failed 0x%x\n", (int)ir);
	i = pthread_create(&thread, NULL, thread_func, run_loop_source);
	if (i)
		fatal(MYNAME ": pthread_create failed %d\n", i);
}

static void
mac_os_deinit(void)
{
	void* unused;
	unsigned i;
	CFRunLoopStop(run_loop);
	pthread_join(thread, &unused);
	(*device)->Release(device);
	xfree(report_buf);
	for (i = sizeofarray(packet_array); i--;) {
		xfree(packet_array[i]);
	}
}

static unsigned
mac_os_packet_read(void* buf)
{
	pthread_mutex_lock(&mutex);
	while (packet_array_head == packet_array_tail) {
		pthread_cond_wait(&cond, &mutex);
	}
	memcpy(buf, packet_array[packet_array_tail++], delbin_os_packet_size);
	packet_array_tail &= sizeofarray(packet_array) - 1;
	pthread_mutex_unlock(&mutex);
	return delbin_os_packet_size;
}

static unsigned
mac_os_packet_write(const void* buf, unsigned size)
{
	IOReturn r = (*device)->setReport(
		device, kIOHIDReportTypeOutput, 0, (void*)buf, size, 2000, NULL, NULL, NULL);
	if (r)
		fatal("setReport failed 0x%x\n", (int)r);
	return size;
}

delbin_os_ops_t delbin_os_ops = {
	mac_os_init,
	mac_os_deinit,
	mac_os_packet_read,
	mac_os_packet_write
};

#endif // __APPLE__

//-----------------------------------------------------------------------------
// libusb
#if HAVE_LIBUSB

#include <usb.h>

static struct usb_device* usb_dev;
static usb_dev_handle* usb_handle;
static int endpoint_in;
static int endpoint_out;

static void
libusb_os_init(const char* fname)
{
	struct usb_bus* bus;
	const struct usb_endpoint_descriptor* endpoint_desc;

	usb_init();
	usb_find_busses();
	usb_find_devices();
	for (bus = usb_busses; usb_dev == NULL && bus; bus = bus->next) {
		struct usb_device* d;
		for (d = bus->devices; d; d = d->next) {
			if (d->descriptor.idVendor == VENDOR_ID && d->descriptor.idProduct == PRODUCT_ID) {
				usb_dev = d;
				break;
			}
		}
	}
	if (usb_dev == NULL) {
		fatal(MYNAME ": no DeLorme PN found\n");
	}
	usb_handle = usb_open(usb_dev);
	if (usb_handle == NULL) {
		fatal(MYNAME ": %s\n", usb_strerror());
	}

	// Device has 1 configuration, 1 interface, 2 interrupt endpoints
	if (usb_claim_interface(usb_handle, 0) < 0) {
#if LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
		if (usb_detach_kernel_driver_np(usb_handle, 0) < 0) {
			warning(MYNAME ": %s\n", usb_strerror());
		}
		if (usb_claim_interface(usb_handle, 0) < 0)
#endif
		{
			const char* s = usb_strerror();
			usb_close(usb_handle);
			fatal(MYNAME ": %s\n", s);
		}
	}
	endpoint_desc = usb_dev->config[0].interface[0].altsetting[0].endpoint;
	delbin_os_packet_size = endpoint_desc[0].wMaxPacketSize;
	endpoint_in = endpoint_desc[0].bEndpointAddress;
	endpoint_out = endpoint_desc[1].bEndpointAddress;
	if ((endpoint_in & USB_ENDPOINT_DIR_MASK) == USB_ENDPOINT_OUT) {
		int t = endpoint_in;
		endpoint_in = endpoint_out;
		endpoint_out = t;
	}
}

static void
libusb_os_deinit(void)
{
	usb_release_interface(usb_handle, 0);
	usb_close(usb_handle);
}

static unsigned
libusb_os_packet_read(void* buf)
{
	int n = usb_interrupt_read(usb_handle, endpoint_in, buf, delbin_os_packet_size, 2000);
	if (n < 0) {
		fatal(MYNAME ": %s\n", usb_strerror());
	}
	return n;
}

static unsigned
libusb_os_packet_write(const void* buf, unsigned size)
{
	int n = usb_interrupt_write(usb_handle, endpoint_out, (char*)buf, size, 2000);
	if (n < 0) {
		fatal(MYNAME ": %s\n", usb_strerror());
	}
	return n;
}

#if HAVE_LINUX_HID
static const delbin_os_ops_t libusb_os_ops =
#else
delbin_os_ops_t delbin_os_ops =
#endif
{
	libusb_os_init,
	libusb_os_deinit,
	libusb_os_packet_read,
	libusb_os_packet_write
};

#endif // HAVE_LIBUSB

//-----------------------------------------------------------------------------
// Linux
#if HAVE_LINUX_HID

#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/hiddev.h>
#include <linux/hidraw.h>

// Read from hidraw, write to hiddev. Reading from hiddev does not work,
// and neither does writing to hidraw.
static int fd_hidraw;
static int fd_hiddev;

static int linuxhid_os_init_status;

static void
linuxhid_os_init(const char* fname)
{
	struct hidraw_devinfo info;
	struct hiddev_field_info finfo;
	DIR* dir = NULL;
	struct dirent* d;

	fd_hidraw = fd_hiddev = -1;
	if (fname && memcmp(fname, "hid:", 4) == 0) {
		char* raw_name = xstrdup(fname + 4);
		char* dev_name = strchr(raw_name, ',');
		if (dev_name == NULL) {
			fatal(MYNAME ": missing hiddev path\n");
		}
		*dev_name++ = 0;
		fd_hidraw = open(raw_name, O_RDONLY);
		if (fd_hidraw < 0) {
			fatal(MYNAME ": %s: %s\n", raw_name, strerror(errno));
		}
		fd_hiddev = open(dev_name, O_WRONLY);
		if (fd_hiddev < 0) {
			fatal(MYNAME ": %s: %s\n", dev_name, strerror(errno));
		}
		xfree(raw_name);
	} else {
		dir = opendir("/dev");
	}
	while (dir && (d = readdir(dir)) != NULL) {
		if (strncmp(d->d_name, "hidraw", 6) == 0) {
			int fd1, fd2;
			char raw_name[32];
			char dev_name[32];
			sprintf(raw_name, "/dev/%s", d->d_name);
			fd1 = open(raw_name, O_RDONLY);
			if (fd1 < 0) {
				if (global_opts.debug_level >= DBGLVL_M)
					warning(MYNAME ": %s: %s\n", raw_name, strerror(errno));
				continue;
			}
			sprintf(dev_name, "/dev/usb/hiddev%s", raw_name + sizeof("/dev/hidraw") - 1);
			fd2 = open(dev_name, O_WRONLY);
			if (fd2 < 0 && errno == ENOENT) {
				sprintf(dev_name, "/dev/hiddev%s", raw_name + sizeof("/dev/hidraw") - 1);
				fd2 = open(dev_name, O_WRONLY);
			}
			if (fd2 < 0) {
				if (global_opts.debug_level >= DBGLVL_M)
					warning(MYNAME ": %s: %s\n", dev_name, strerror(errno));
				close(fd1);
				continue;
			}
			if (ioctl(fd1, HIDIOCGRAWINFO, &info) == 0 &&
			    info.vendor == VENDOR_ID && info.product == PRODUCT_ID)
			{
				fd_hidraw = fd1;
				fd_hiddev = fd2;
				break;
			}
			close(fd1);
			close(fd2);
		}
	}
	if (dir) {
		closedir(dir);
	}
	if (fd_hidraw < 0) {
		if (linuxhid_os_init_status == 0)
			fatal(MYNAME ": no DeLorme PN found\n");
		return;
	}
	finfo.report_type = HID_REPORT_TYPE_INPUT;
	finfo.report_id = 0;
	finfo.field_index = 0;
	if (ioctl(fd_hiddev, HIDIOCGFIELDINFO, &finfo) < 0) {
		warning(MYNAME ": HIDIOCGFIELDINFO: %s\n", strerror(errno));
		if (linuxhid_os_init_status == 0)
			exit(1);
		return;
	}
	delbin_os_packet_size = finfo.maxusage;
	linuxhid_os_init_status = 0;
}

static void
linuxhid_os_deinit(void)
{
	close(fd_hidraw);
	close(fd_hiddev);
}

static unsigned
linuxhid_os_packet_read(void* buf)
{
	int n = read(fd_hidraw, buf, delbin_os_packet_size);
	if (n < 0) {
		fatal(MYNAME ": %s\n", strerror(errno));
	}
	return n;
}

static unsigned
linuxhid_os_packet_write(const void* buf, unsigned size)
{
	struct hiddev_usage_ref_multi urefm;
	struct hiddev_report_info rinfo;
	const gbuint8* p = buf;
	unsigned i;

	for (i = 0; i < size; i++) {
		urefm.values[i] = p[i];
	}
	urefm.num_values = size;
	memset(&urefm.uref, 0, sizeof(urefm.uref));
	urefm.uref.report_type = HID_REPORT_TYPE_OUTPUT;
	if (ioctl(fd_hiddev, HIDIOCSUSAGES, &urefm)) {
		fatal(MYNAME ": HIDIOCSUSAGES: %s\n", strerror(errno));
	}
	memset(&rinfo, 0, sizeof(rinfo));
	rinfo.report_type = HID_REPORT_TYPE_OUTPUT;
	if (ioctl(fd_hiddev, HIDIOCSREPORT, &rinfo)) {
		fatal(MYNAME ": HIDIOCSREPORT: %s\n", strerror(errno));
	}
	return size;
}

static const delbin_os_ops_t linuxhid_os_ops = {
	linuxhid_os_init,
	linuxhid_os_deinit,
	linuxhid_os_packet_read,
	linuxhid_os_packet_write
};

static void
linux_os_init(const char* fname)
{
	// tell linuxhid_os_init not to exit
	linuxhid_os_init_status = 1;
	linuxhid_os_init(fname);
	if (linuxhid_os_init_status == 0) {
		delbin_os_ops = linuxhid_os_ops;
	} else {
#if HAVE_LIBUSB
		if (global_opts.debug_level >= DBGLVL_M)
			warning(MYNAME ": HID init failed, falling back to libusb\n");
		delbin_os_ops = libusb_os_ops;
		delbin_os_ops.init(fname);
#else
		fatal(MYNAME ": no DeLorme PN found\n");
#endif
	}
}

delbin_os_ops_t delbin_os_ops = {
	linux_os_init,
	NULL,
	NULL,
	NULL
};

#endif // HAVE_LINUX_HID

//-----------------------------------------------------------------------------
// stubs
#if !(_WIN32 || __APPLE__ || HAVE_LIBUSB || HAVE_LINUX_HID)
static void
stub_os_init(const char* fname)
{
	fatal(MYNAME ": OS not supported\n");
}
static void
stub_os_deinit(void)
{
}
static unsigned
stub_os_packet_read(void* buf)
{
	return 0;
}
static unsigned
stub_os_packet_write(const void* buf, unsigned size)
{
	return 0;
}
delbin_os_ops_t delbin_os_ops = {
	stub_os_init,
	stub_os_deinit,
	stub_os_packet_read,
	stub_os_packet_write
};
#endif
// end OS device I/O implementations section
//=============================================================================

static const int track_color_bgr[] = {
	0x0000ff, // red
	0x00ffff, // yellow
	0x008000, // green
	0xff0000, // blue
	0x808080, // gray
	0xffffff, // white
	0,        // black
	0xffff00, // cyan
	0xff00ff, // magenta
	0x00a5ff, // orange
	0x82004b, // indigo
	0xeea5ee  // violet
};

static int track_color(unsigned i)
{
	int bgr = -1;
	if (i < sizeofarray(track_color_bgr)) {
		bgr = track_color_bgr[i];
	}
	return bgr;
}

static unsigned track_color_index(int bgr)
{
	unsigned i = sizeofarray(track_color_bgr);
	do {
		i--;
	} while (i != 0 && track_color_bgr[i] != bgr);
	return i;
}

static const char* const waypoint_symbol_name[] = {
	// 0
	"Red Map Pin",
	"Dark Red Map Pin",
	"Yellow Map Pin",
	"Dark Yellow Map Pin",
	"Green Map Pin",
	"Dark Green Map Pin",
	"Turquoise Map Pin",
	"Dark Turquoise Map Pin",
	"Blue Map Pin",
	"Dark Blue Map Pin",
	// 10
	"Gray Map Pin",
	"Dark Gray Map Pin",
	"Red Flag",
	"Dark Red Flag",
	"Yellow Flag",
	"Dark Yellow Flag",
	"Green Flag",
	"Dark Green Flag",
	"Turquoise Flag",
	"Dark Turquoise Flag",
	// 20
	"Blue Flag",
	"Dark Blue Flag",
	"Gray Flag",
	"Dark Gray Flag",
	"Red Dot",
	"Dark Red Dot",
	"Yellow Dot",
	"Dark Yellow Dot",
	"Green Dot",
	"Dark Green Dot",
	// 30
	"Turquoise Dot",
	"Dark Turquoise Dot",
	"Blue Dot",
	"Dark Blue Dot",
	"Gray Dot",
	"Dark Gray Dot",
	"Small Red Dot",
	"Small Dark Red Dot",
	"Small Yellow Dot",
	"Small Dark Yellow Dot",
	// 40
	"Small Green Dot",
	"Small Dark Green Dot",
	"Small Turquoise Dot",
	"Small Dark Turquoise Dot",
	"Small Blue Dot",
	"Small Dark Blue Dot",
	"Small Gray Dot",
	"Small Dark Gray Dot",
	"Arrow Up",
	"Arrow Down",
	// 50
	"Arrow Left",
	"Arrow Right",
	"Arrow Up Left",
	"Arrow Up Right",
	"Arrow Down Left",
	"Arrow Dow Right",
	"Green Star",
	"Yellow Square",
	"Red X",
	"Turquoise Circle",
	// 60
	"Purple Triangle",
	"American Flag",
	"Stop",
	"Parking",
	"First Aid",
	"Dining",
	"Railroad Crossing",
	"Heliport",
	"Restroom",
	"Information",
	// 70
	"Diver Down",
	"Exit",
	"Health Facility",
	"Police",
	"Post Office",
	"Mining",
	"Danger",
	"Money",
	"Exclamation",
	"Car",
	// 80
	"Jeep",
	"Truck",
	"Tow Truck",
	"Motor Home",
	"School Bus",
	"Four-wheeler",
	"Snowmobile",
	"Sailboat",
	"Powerboat",
	"Boat Launch",
	// 90
	"Anchor",
	"Buoy",
	"Shipwreck",
	"Glider Area",
	"Private Airport",
	"Public Airport",
	"Military Airport",
	"Military Base",
	"House",
	"Church",
	// 100
	"Building",
	"School",
	"Lighthouse",
	"Bridge",
	"Radio Tower",
	"Dam",
	"Tunnel",
	"Toll Booth",
	"Gas Station",
	"Lodging",
	// 110
	"Telephone",
	"Traffic Light",
	"Fire Hydrant",
	"Tombstone",
	"Picnic Table",
	"Tent",
	"Shelter",
	"Camper",
	"Fire",
	"Shower",
	// 120
	"Drinking Water",
	"Binoculars",
	"Camera",
	"Geocache",
	"Geocache Found",
	"Fishing Pole",
	"Ice Fishing Trap Set",
	"Ice Fishing Trap Up",
	"Moose",
	"Deer",
	// 130
	"Bear",
	"Bird",
	"Duck",
	"Fish",
	"Deer Tracks",
	"Animal Tracks",
	"Bird Tracks",
	"Birch Tree",
	"Evergreen Tree",
	"Deciduous Tree",
	// 140
	"Flower Garden",
	"Mountain",
	"Cave",
	"Beach",
	"Hiking",
	"Swimming",
	"Bicycling",
	"Kayaking",
	"Canoeing",
	"Water Skiing",
	// 150
	"Cross-country Skiing",
	"Downhill Skiing",
	"Ice Skating",
	"Dogsledding",
	"Shooting",
	"Golf Course",
	"Ballpark",
	// 157-182 added in PN-40 2.5 firmware
	"Cache Found",
	"Didn't Find It",
	"My Cache",
	// 160
	"Traditional Cache",
	"Multi-Cache",
	"Unknown Cache",
	"Letterbox Hybrid",
	"Whereigo Cache",
	"Event Cache",
	"Mega-Event Cache",
	"Cache In Trash Out Event",
	"EarthCache",
	"Virtual Cache",
	// 170
	"Webcam Cache",
	"Waymark",
	"NGS Benchmark",
	"Write Note",
	"Needs Maintenance",
	"Final Location",
	"Parking Area",
	"Question to Answer",
	"Reference Point",
	"Stages of a Multicache",
	// 180
	"Trailhead",
	"Temporarily Disable Listing",
	"Enable Listing"
};

static const char*
waypoint_symbol(unsigned i)
{
	const char* p = NULL;
	if (i < sizeofarray(waypoint_symbol_name)) {
		p = waypoint_symbol_name[i];
	}
	return p;
}

static unsigned
waypoint_symbol_index(const char* name)
{
	static unsigned last_result;
	static char last_name[32];
	unsigned i = last_result;

	if (strncmp(name, last_name, sizeof(last_name)) != 0) {
		i = sizeofarray(waypoint_symbol_name);
		do {
			i--;
		} while (i != 0 && case_ignore_strcmp(name, waypoint_symbol_name[i]) != 0);
		strncpy(last_name, name, sizeof(last_name));
		last_result = i;
	}
	return i;
}

// vi: ts=4 sw=4 noexpandtab
